package smallfrench.ui;

import java.awt.Color;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.RenderingHints;
import java.awt.Toolkit;
import java.awt.datatransfer.StringSelection;
import java.awt.event.ComponentEvent;
import java.awt.event.ComponentListener;
import java.awt.event.FocusEvent;
import java.awt.event.FocusListener;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.TimeUnit;

import javax.swing.JPanel;
import javax.swing.JPopupMenu;
import javax.swing.event.PopupMenuEvent;
import javax.swing.event.PopupMenuListener;

import smallfrench.moteur.interp.IConsole;

/**
 * Petite console pour les entrées/sorties.
 * 
 * @author antoine1023
 * 
 */
@SuppressWarnings("serial")
public class Console extends JPanel implements IConsole, Selectionnable {

	/**
	 * Les entrées clavier
	 */
	private BlockingQueue<KeyEvent> tamponEntree;

	/**
	 * Etat des touches (<code>true</code> si enfoncée)
	 */
	private boolean[] touches = new boolean[256];

	/**
	 * Tampon de la sortie. <br>
	 * 0 1 2 3<br>
	 * 4 5 6 7<br>
	 * 8 9 ...<br>
	 */
	private char[] tampon;

	private Color[] tamponPremierPlan;
	private Color[] tamponArrierePlan;

	/**
	 * Le nombre de caractères d’une colonne.
	 */
	private int hauteur;

	/**
	 * Le nombre de caractères d’une ligne.
	 */
	private int largeur;

	private Font police;
	private int hauteurCaractere;
	private int largeurCaractere;
	private int descent;

	/**
	 * Position du curseur
	 */
	private int curseurX;
	/**
	 * Position du curseur
	 */
	private int curseurY;

	/**
	 * Couleur du premier plan courante
	 */
	private Color premierPlan = Color.WHITE;

	/**
	 * Couleur de l’arrière plan courante
	 */
	private Color arrierePlan = Color.BLACK;

	/**
	 * <code>null</code> si aucune selection.
	 */
	private Point debutSelection, finSelection;

	/**
	 * Le menu contextuel
	 */
	private JPopupMenu popupMenu;

	/** <code>true</code> si un évenement rafraichissement est en cours */
	private boolean rafraichissement;

	// ---------------------------------------------------------

	public Console() {

		tamponEntree = new ArrayBlockingQueue<KeyEvent>(1000);

		setOpaque(true);
		setFocusable(true);

		addFocusListener(new FocusListener() {
			@Override
			public void focusLost(FocusEvent e) {
				reinitialiserEtatsTouches();
			}

			@Override
			public void focusGained(FocusEvent e) {
				Actions.setSelectionnableActif(Console.this);
				reinitialiserEtatsTouches();
			}
		});

		addComponentListener(new ComponentListener() {
			@Override
			public void componentShown(ComponentEvent arg0) {}

			@Override
			public void componentResized(ComponentEvent arg0) {
				actuTailleTampon();
			}

			@Override
			public void componentMoved(ComponentEvent arg0) {}

			@Override
			public void componentHidden(ComponentEvent arg0) {}
		});

		police = Ressources.getPoliceMonospace().deriveFont(14f);
		FontMetrics fm = getFontMetrics(police);
		hauteurCaractere = fm.getHeight();
		largeurCaractere = fm.charWidth(' ');
		descent = fm.getDescent();
		setFont(police);

		addKeyListener(new KeyListener() {
			@Override
			public void keyTyped(KeyEvent e) {
			}

			@Override
			public void keyReleased(KeyEvent e) {
				int touche = e.getKeyCode();
				if (touche <= touches.length)
					touches[touche] = false;
			}

			@Override
			public void keyPressed(KeyEvent e) {
				int touche = e.getKeyCode();
				if (touche <= touches.length)
					touches[touche] = true;
				tamponEntree.add(e);
			}
		});

		setBackground(Color.BLACK);

		popupMenu = new JPopupMenu();
		popupMenu.add(Actions.COPIER);
		popupMenu.add(Actions.TOUT_SEL);

		popupMenu.addPopupMenuListener(new PopupMenuListener() {
			@Override
			public void popupMenuWillBecomeVisible(PopupMenuEvent e) {}

			@Override
			public void popupMenuWillBecomeInvisible(PopupMenuEvent e) {
				rafraichir();
			}

			@Override
			public void popupMenuCanceled(PopupMenuEvent e) {}
		});

		addMouseListener(new MouseListener() {
			@Override
			public void mouseReleased(MouseEvent e) {}

			@Override
			public void mousePressed(MouseEvent e) {
				requestFocusInWindow();
			}

			@Override
			public void mouseExited(MouseEvent e) {}

			@Override
			public void mouseEntered(MouseEvent e) {}

			@Override
			public void mouseClicked(MouseEvent e) {

				if (e.getButton() == MouseEvent.BUTTON3) {
					// Bouton droit
					popupMenu.show(Console.this, e.getX(), e.getY());
				} else {
					debutSelection = finSelection = null;
					rafraichir();
				}
			}
		});

		addMouseMotionListener(new MouseMotionListener() {
			@Override
			public void mouseMoved(MouseEvent e) {}

			@Override
			public void mouseDragged(MouseEvent e) {

				if (debutSelection == null)
					debutSelection = vueVersModele(e.getPoint());

				finSelection = vueVersModele(e.getPoint());
				rafraichir();
			}
		});

	}

	// ---------------------------------------------------------

	@Override
	public void paint(Graphics graphics) {
		Graphics2D g = (Graphics2D) graphics;
		g.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING,
				RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
		g.setRenderingHint(RenderingHints.KEY_RENDERING,
				RenderingHints.VALUE_RENDER_QUALITY);

		g.setColor(Color.BLACK);
		g.fillRect(0, 0, getWidth(), getHeight());

		g.setFont(police);

		int iDebutSel = 0, iFinSel = 0;
		if (debutSelection != null) {
			int a = debutSelection.y * largeur + debutSelection.x;
			int b = finSelection.y * largeur + finSelection.x;
			iDebutSel = Math.min(a, b);
			iFinSel = Math.max(a, b);
		}

		for (int i = 0 ; i < hauteur ; i++) {

			for (int j = 0 ; j < largeur ; j++) {

				int index = i * largeur + j;

				Color couleurPP = tamponPremierPlan[index];
				g.setColor(couleurPP);

				if (debutSelection != null
						&& index >= iDebutSel && index <= iFinSel) {

					g.fillRect(
							j * largeurCaractere,
							i * hauteurCaractere + descent,
							largeurCaractere, hauteurCaractere);

					g.setColor(Color.BLACK);
				}
				g.drawChars(tampon, index, 1,
						j * largeurCaractere, (i + 1) * hauteurCaractere);
			}
		}

		rafraichissement = false;
	}

	/**
	 * 
	 * @return Le texte actuellement sélectionné ou <code>null</code>
	 */
	public String getSelection() {
		if (debutSelection == null) return null;

		int iDebutSel = 0, iFinSel = 0;
		int a = debutSelection.y * largeur + debutSelection.x;
		int b = finSelection.y * largeur + finSelection.x;
		iDebutSel = Math.min(a, b);
		iFinSel = Math.max(a, b);

		return new String(tampon, iDebutSel, iFinSel - iDebutSel + 1);
	}

	/**
	 * Mise à jour de la taille du tampon de sortie, effacement de la
	 * sélection.
	 */
	private void actuTailleTampon() {
		int vLargeur = largeur;
		int vHauteur = hauteur;

		largeur = getWidth() / largeurCaractere;
		hauteur = getHeight() / hauteurCaractere;

		// On sauvegarde les vieux tampons
		Color[] vTamponPremierPlan = tamponPremierPlan;
		Color[] vTamponArrierePlan = tamponArrierePlan;
		char[] vieuxTampon = tampon;

		tampon = new char[largeur * hauteur];
		tamponPremierPlan = new Color[largeur * hauteur];
		tamponArrierePlan = new Color[largeur * hauteur];

		// On initialise nouveaux tampons
		for (int i = 0 ; i < hauteur ; i++) {
			for (int j = 0 ; j < largeur ; j++) {
				int index = i * largeur + j;
				tampon[index] = ' ';
				tamponPremierPlan[index] = premierPlan;
				tamponArrierePlan[index] = arrierePlan;
			}
		}

		if (vieuxTampon != null) {
			// On essaye de récupérer ce que l’on peut

			int largeurMin = Math.min(largeur, vLargeur);
			int hauteurMin = Math.min(hauteur, vHauteur);

			for (int i = 0 ; i < hauteurMin ; i++) {
				// pour chaque ligne...

				for (int j = 0 ; j < largeurMin ; j++) {

					int vIndex = i * vLargeur + j;
					int index = i * largeur + j;

					tampon[index] = vieuxTampon[vIndex];
					tamponPremierPlan[index] = vTamponPremierPlan[vIndex];
					tamponArrierePlan[index] = vTamponArrierePlan[vIndex];
				}
			}

			curseurX = Math.min(curseurX, largeur - 1);
			curseurY = Math.min(curseurY, hauteur - 1);
		} else {
			curseurX = curseurY = 0;
		}

		debutSelection = finSelection = null;

		rafraichir();
	}

	@Override
	public void effacer() {
		for (int i = 0 ; i < hauteur ; i++) {
			for (int j = 0 ; j < largeur ; j++) {

				int index = i * largeur + j;

				tampon[index] = ' ';
				tamponPremierPlan[index] = premierPlan;
				tamponArrierePlan[index] = arrierePlan;
			}
		}
		curseurX = curseurY = 0;
		rafraichir();
	}

	/**
	 * Efface et réinitialise les couleurs d’arrière plan et de premier
	 * plan.
	 */
	public void reinitialiser() {
		effacer();
		premierPlan = Color.WHITE;
		arrierePlan = Color.BLACK;
	}

	@Override
	public void viderTamponEntree() {
		tamponEntree.clear();
	}

	private void reinitialiserEtatsTouches() {
		for (int i = 0; i < touches.length; i++) {
			touches[i] = false;
		}
	}

	@Override
	public void ecrire(char c) {
		if (hauteur == 0)
			return;

		if (c == '\n') {
			ecrireLigne();
			return;
		}

		if (curseurX >= largeur) {
			curseurX = 0;
			ecrireLigne();
		}

		int index = curseurY * largeur + curseurX;

		tampon[index] = c;
		tamponPremierPlan[index] = premierPlan;

		curseurX++;

		rafraichir();
	}

	@Override
	public void ecrire(String s) {
		for (int i = 0; i < s.length(); i++) {
			ecrire(s.charAt(i));
		}
	}

	@Override
	public void ecrireLigne() {
		curseurY++;
		if (curseurY >= hauteur) {
			if (hauteur == 0) {
				curseurY = 0;
			} else {
				curseurY = hauteur - 1;
				// on fait défiler
				for (int i = 1 ; i < hauteur ; i++) {
					System.arraycopy(tampon, i * largeur,
							tampon, (i - 1) * largeur,
							largeur);
					System.arraycopy(tamponPremierPlan, i * largeur,
							tamponPremierPlan, (i - 1) * largeur,
							largeur);
					System.arraycopy(tamponArrierePlan, i * largeur,
							tamponArrierePlan, (i - 1) * largeur,
							largeur);
				}
				// ... et on n’oublie pas d’effacer la ligne du bas
				for (int i = 0 ; i < largeur ; i++) {
					int index = (hauteur - 1) * largeur + i;
					tampon[index] = ' ';
					tamponPremierPlan[index] = premierPlan;
					tamponArrierePlan[index] = arrierePlan;
				}
			}
		}
		curseurX = 0;
		rafraichir();
	}

	@Override
	public int getLargeur() {
		return largeur;
	}

	@Override
	public int getHauteur() {
		return hauteur;
	}

	@Override
	public boolean caracteresDisponibles() {
		return ! tamponEntree.isEmpty();
	}

	@Override
	public char lireCaractere() throws InterruptedException {
		for (;;) {
			KeyEvent e = tamponEntree.poll(1, TimeUnit.HOURS);
			char c = e.getKeyChar();
			if (c != KeyEvent.CHAR_UNDEFINED)
				return c;
		}
	}

	@Override
	public int lireTouche() {
		for (int i = 0 ; i < touches.length ; i++) {
			if (touches[i])
				return i;
		}
		return -1;
	}

	@Override
	public void ecrireLigne(char c) {
		ecrire(c);
		ecrireLigne();
	}

	@Override
	public void ecrireLigne(String s) {
		ecrire(s);
		ecrireLigne();
	}

	@Override
	public void setPremierPlan(Color couleur) {
		premierPlan = couleur;
	}

	@Override
	public void setPositionCurseur(int x, int y) {
		if (x >= 0 && x < largeur
				&& y >= 0 && y < hauteur) {
			curseurX = x;
			curseurY = y;
		}
	}

	/**
	 * 
	 * @param pixels Les coordonnées d’un pixel du composant.
	 * @return Les coordonnées du caractère pointé.
	 */
	public Point vueVersModele(Point pixels) {
		return new Point(
				Math.min(Math.max(pixels.x / largeurCaractere, 0), largeur - 1),
				Math.min(Math.max(pixels.y / hauteurCaractere, 0), hauteur - 1)
				);
	}

	/**
	 * Copie le texte sélectionné dans le presse-papiers de la console
	 */
	@Override
	public void copier() {
		StringSelection s = new StringSelection(getSelection());
		Toolkit.getDefaultToolkit().getSystemClipboard().setContents(s, s);
	}

	/**
	 * Sélectionne tout le texte de la console
	 */
	@Override
	public void toutSelectionner() {
		debutSelection = new Point(0, 0);
		finSelection = new Point(largeur - 1, hauteur - 1);
	}

	/**
	 * Demande un rafraichissement si nécéssaire
	 */
	public void rafraichir() {
		if (! rafraichissement) {
			rafraichissement = true;
			repaint();
		}
	}

}
